Vue进阶知识(2)

(一) webpack配置

(1) 前端项目构建打包工具介绍

  1. 为什么需要这些项目构建打包工具
  2. 项目构建打包工具有哪些
    • grunt
    • gulp(流)
    • fis
    • rollup
    • webpack(万物皆模块)
    • vite(快)
  3. webpack介绍
    1. 是一款打包构建工具,目前就流行打包构建工具
    2. webpack特点: 一切皆模块, 能把所有资源打包成浏览器能识别的 html,css,js,png
    3. 官网地址: https://webpack.docschina.org/

(2) 一份webpack配置

核心知识点:

  1. 入口和出口
  2. loader: webpack默认只认识js模块, 其它文件都要配置响应的loader
  3. plugin: 可以给webpack添加额外的功能
  4. resolve: 可以配置一写特殊的功能
  5. devServer 启动服务,也可在它里面配置跨域
const path = require("path");
const htmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  entry: "./src/main.js",
  output: {
    filename: "app.js",
    path: path.resolve(__dirname, "dist"),
  },
  mode: "development",
  // 对模块进行配置
  module: {
    // 规则
    rules: [
      {
        // 意思是: 遇到.css结尾的文件, 先使用style-loader和css-loader进行处理
        test: /\.css$/,
        use: ["style-loader", "css-loader"],
      },
      {
        test: /\.less$/,
        use: ["style-loader", "css-loader", "less-loader"],
      },
    ],
  },

  plugins: [new htmlWebpackPlugin()],

  resolve: {
    alias: {
      "@": path.resolve(__dirname, "src"),
    },
    // 省略后缀名
    extensions: [".mjs", ".js", ".jsx", ".vue", ".json", ".wasm"],
  },

  devServer: { 
    compress: true,
    open:true, //是否自动打开默认浏览器
    port: 8080 // 端口号
  }
};

(3) vue-cli是如何创建项目的

  1. vue-cli底层使用的就是webpack来构建和打包项目的, 但是它把webpack的配置都隐藏起来了

  2. 查看vue-cli中的webpack配置

    vue inspect > webpack.config.js
    

(三) vue配置跨域

 // 代理
    proxy: {
      // 所有包含有'api'字符串的请求地址都会转发到target指向的地址
      "/api": {
        target: "http://81.71.65.4:3003",
        ws: true,
        // 允许跨域
        changeOrigin: true,
        pathRewrite: {
          "^/api": "", //通过pathRewrite重写地址,将前缀/api转为/
        },
      },
    },

(四) 统一管理请求

/src/api/index.js

import $axios from '../utils/request';
 
// account模块
export const $accountAdd = (data)=> {
    return $axios.post('/account/add',data);
}
export const $accountList = ()=> {
    return $axios.get('/account/list');
}
export const $accountLogin = (params)=> {
    return $axios.post('/account/login',params);
}
export const $accountDel = (data={})=> {
    return $axios.post('/account/del',data);

使用

<script>
import * as api from "../../api/index";
export default {
  data() {
    return {
      list: [],
    };
  },
  created() {
    this.getList();
  },

  methods: {  
    getList() { 
      api.$feeUsageList().then((res) => {
        this.list = res.data; 
      });
    },
  }
};
</script>

(五) 路由守卫

常常用来鉴权(鉴察权限)

// 路由白名单
const whiteList = ["/login"];
/**
 * 路由守卫
 * to 要前往的路由
 * from 当前路由
 * next 下一步操作
 */
router.beforeEach((to, from, next) => {
  if (store.state.token) {
    //已经登录: 如果路由是登录页,默认跳转到首页
    if (to.path == "/login") {
      next({
        path: "/dashboard/index",
      });
    } else {
      next();
    }
  } else {
    //没有登录,路由是否在白名单中
    if (whiteList.includes(to.path)) {
      //放行
      next();
    } else {
      //没在白名单
      next("/login");
    }
  }
});

export default router;

(六) 用户权限管理

  1. 根据不同角色配置不同的路由数组

    注: 有些公司由后台配置, 但由前端来配置更灵活更方便

  2. 登录获取token

  3. 根据用户角色通过router.addRoutes动态添加用户角色对应的路由数组, 并把数组保存到store里

  4. 从store里获取路由数组, 动态渲染侧边栏

(七) 自定义vue指令

一个简单的vue指令(是字体变红色)

<template>
  <div>
    <span v-red> welcome</span>
  </div>
</template>

<script>
export default {  
  directives: {
    red: {
      // el就是使用v-red指令的那个标签
      inserted(el) {
        el.style.color = "red";
      },
    },
  },
};
</script>

vue指令的三个构造函数:

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
  • update:所在组件的 VNode 更新时调用

(八) vue双向数据绑定原理

https://zhuanlan.zhihu.com/p/29079569

此问题回答的关键点:

  1. 双向数据绑定和兴原理
  2. vue双向数据绑定的实现过程

(1) 双向数据绑定核心原理

vue数据双向绑定是通过es5的一个新增的一个特性Object.defineProperty来对数据进行劫持, 然后结合发布者-订阅者模式的方式来实现的,其中比较关键的是数据劫持(Object.defineProperty),下面咱们看一个例子。

var obj = {}
Object.defineProperty(obj, 'name', {
    get: function () {
        console.log("获取了");
    },
    set: function () {
        console.log('修改了');
    }
})
// 修改属性
obj.name = 'fei';
// 读取属性
var name = obj.name;

有了Object.defineProperty, 我们就可以在获取和修改属性的时候做一些响应的操作, 从而实现数据的双向绑定, 下面是一个简单的版的双向数据绑定

<body>
    <div id="app">
        <input type="text" oninput="handleInput()" id="inp">
        <p id="text"></p>
    </div>

    <script>
        var obj = {};
        var $inp = document.querySelector('#inp');
        var $text = document.querySelector('#text');
        Object.defineProperty(obj, 'username', {
            set: function (value) {
                $inp.value = value;
                $text.innerText = value;
            },
        })

        function handleInput() {
            obj.username = $inp.value;;
        }
    </script>
</body>

(2) Vue的双向数据绑定原理

实现过程

​ 我们已经知道实现数据的双向绑定,首先要对数据进行劫持监听,所以我们需要设置一个监听器Observer,用来监听所有属性。如果属性发上变化了,就需要告诉订阅者Watcher看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的。接着,我们还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令对应初始化成一个订阅者Watcher,并替换模板数据或者绑定相应的函数,此时当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。因此接下去我们执行以下3个步骤,实现数据的双向绑定:

  1. 实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。

  2. 实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。

  3. 实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。

手写实现vue双向数据绑定

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>vue双向数据绑定</title>
</head>

<body>

    <div id="app">
        {{username}} <br>
        <input type="text" v-model="msg"> <br>
        {{ msg }}
    </div>

    <script>
        /**
         * 第1步: 实现一个监听器Observer
         * 用来劫持并监听所有属性
         * 如果属性有变动的,就通知订阅者。
         */
        function Observe(obj, vm) {
            Object.keys(obj).forEach(function (key) {
                defineReactive(vm, key, obj[key]);
            })
        }

        function defineReactive(obj, key, val) {
            var dep = new Dep();
            // 劫持数据
            Object.defineProperty(obj, key, {
                get: function () {
                    // 添加订阅者 watcher 到主题对象 Dep
                    if (Dep.target) dep.addSub(Dep.target);
                    return val
                },
                set: function (newVal) {
                    // 新数据和旧数据相同, 不做处理
                    if (newVal === val) return;
                    val = newVal;
                    // 作为发布者发出通知
                    dep.notify();
                }
            });
        }

        function nodeToFragment(node, vm) {
            var flag = document.createDocumentFragment();
            var child;
            // 许多同学反应看不懂这一段,这里有必要解释一下
            // 首先,所有表达式必然会返回一个值,赋值表达式亦不例外
            // 理解了上面这一点,就能理解 while (child = node.firstChild) 这种用法
            // 其次,appendChild 方法有个隐蔽的地方,就是调用以后 child 会从原来 DOM 中移除
            // 所以,第二次循环时,node.firstChild 已经不再是之前的第一个子元素了
            while (child = node.firstChild) {
                compile(child, vm);
                flag.appendChild(child); // 将子节点劫持到文档片段中
            }

            return flag
        }

        /**
         * 第2步: 实现一个订阅者Watcher
         * 可以收到属性的变化通知并执行相应的函数,从而更新视图。
         */
        function Watcher(vm, node, name, nodeType) {
            Dep.target = this;
            this.name = name;
            this.node = node;
            this.vm = vm;
            this.nodeType = nodeType;
            // 调用更新视图的方法去更新视图
            this.update();
            Dep.target = null;
        }

        Watcher.prototype = {
            update: function () {
                this.get();
                if (this.nodeType == 'text') {
                    this.node.nodeValue = this.value;
                }
                if (this.nodeType == 'input') {
                    this.node.value = this.value;
                }
            },
            // 获取 data 中的属性值
            get: function () {
                this.value = this.vm[this.name]; // 触发相应属性的 get
            }
        }

        // 因为订阅者可能有很多个, 所以需要一个消息收集器
        function Dep() {
            this.subs = []
        }
        Dep.prototype = {
            addSub: function (sub) {
                this.subs.push(sub);
            },

            notify: function () {
                this.subs.forEach(function (sub) {
                    sub.update();
                });
            }
        }

        /**
         * 第3步: 实现一个解析器Compile
         * 可以扫描和解析每个节点的相关指令
         * 并根据初始化模板数据以及初始化相应的订阅器。
         */
        function compile(node, vm) {
            var reg = /\{\{(.*)\}\}/;
            // 节点类型为元素
            if (node.nodeType === 1) {
                var attr = node.attributes;
                // 解析属性
                for (var i = 0; i < attr.length; i++) {
                    if (attr[i].nodeName == 'v-model') {
                        var name = attr[i].nodeValue; // 获取 v-model 绑定的属性名
                        node.addEventListener('input', function (e) {
                            // 给相应的 data 属性赋值,进而触发该属性的 set 方法
                            vm[name] = e.target.value;
                        });
                        node.value = vm[name]; // 将 data 的值赋给该 node
                        node.removeAttribute('v-model');
                    }
                };

                new Watcher(vm, node, name, 'input');
            }
            // 节点类型为 text
            if (node.nodeType === 3) {
                if (reg.test(node.nodeValue)) {
                    var name = RegExp.$1; // 获取匹配到的字符串
                    name = name.trim();
                    new Watcher(vm, node, name, 'text');
                }
            }
        }


        function Vue(options) {
            this.data = options.data;
            var data = this.data;
            Observe(data, this);
            var id = options.el;
            var dom = nodeToFragment(document.getElementById(id), this);
            // 编译完成后,将 dom 返回到 app 中
            document.getElementById(id).appendChild(dom);
        }

        var vm = new Vue({
            el: 'app',
            data: {
                username: '张三',
                msg: 'hello world'
            }
        })
    </script>
</body> 
</html>

(九) keep-alive

  1. 用户在某个列表页面选择筛选条件过滤出一份数据列表,由列表页面进入数据详情页面,再返回该列表页面,我们希望:列表页面可以保留用户的筛选(或选中)状态。

  2. keep-alive就是用来解决这种场景。当然keep-alive不仅仅是能够保存页面/组件的状态这么简单,它还可以避免组件反复创建和渲染,有效提升系统性能。总的来说,keep-alive用于保存组件的渲染状态。

  3. keep-alive的生命周期

    1. 初次进入时:created > mounted > activated;退出后触发 deactivated
    2. 再次进入:会触发 activated;事件挂载的方法等,只执行一次的放在 mounted 中;组件每次进去执行的方法放在 activated 中
  4. 应用实例

    注意: 要把keep-alive放在父路由上, 不然不生效

    // 例子1: 缓存所有组件

    <keep-alive>
       <router-view/>
    </keep-alive>
    

    // 例子2: 缓存部分组件

    // router.js
    {
        path: "/demo",
        name: "demo",
        meta: {
          title: "demo",
          icon: "icon-d-right-arrow",
        },
        hidden: isDev ? false : true,
        component: Layout,
        redirect: "/demo/index",
        children: [
          {
            path: "index",
            name: "demo-index",
            meta: {keepAlive: true,  // true代表需要缓存,必须放入meta里, 不然访问不到,
            component: () => import("@/views/demo/index.vue"),
          },
        ],
      },
            
    <!-- 父路由组件(在这里是Layout)添加一下keep-alive -->       
    <!-- 对keepAlive为true的路由使用keep-alive包裹 -->          
    <keep-alive>
      <router-view v-if="$route.meta.keepAlive" />
    </keep-alive>
    <router-view v-if="!$route.meta.keepAlive" />         
    

总结:

  1. 被缓存的组件, created和mounted只会调用一次, 第二次进入组件不再调用(强制刷新除外)
  2. 被缓存的组件有额外的两个生命周期activated和deactivated, 如果你进入或离开组件需要做一些操作, 可以在这两个生命周期里进行

(十) 组件通信方式总结

(1) 父子组件通信(略)

(2) vuex跨组件通信(略)

(3) provide和inject 祖宗和后代通信

  1. 祖宗使用provide定义数据或方法
  2. 后代(任何层级)都可以使用inject获取数据
// 祖宗组件, App.vue
<script>
export default {
  provide: {
    username: '张三',
    say() {
      console.log(this.username+'是狂徒');
    }
  }
}
</script>

// 后代组件
<template>
  <div>provide和inject</div>
</template>

<script> 
export default {
  inject: ["username", "say"],
  created() {
    console.log(this.username);
    this.say();
  },
};
</script> 

(3) vue中央事件总线机制(bus)

  1. vue中非父子组件之间通信除了使用vuex,也可以通过bus总线,两者适用场景不同。

  2. vuex适用中大型项目、数据在多组件之间公用的情况。

  3. bus的本质是创建了一个空的vue实例用来存放数据, 适合小项目、数据被更少组件使用的项目,对于中大型项目 数据在很多组件之间使用的情况 bus就不太适用了。bus其实就是一个发布订阅模式,利用vue的自定义事件机制,在触发的地方通过$emit向外发布一个事件,在需要监听的页面,通过$on监听事件(订阅)

    注意:

    • 需要先订阅, 发布的时候才能收到
    • 比较适合兄弟组件通信, 也就是一个组件有很多子组件, 这些子组件之间的通信
    • 也适合孙子组件发送消息个祖宗组件, 因为祖宗组件会比后代组件先加载

概念: 发布订阅模式, 就好像以前一个家庭跟一个报社订报纸, 报社是发布者, 有了新的报纸就派人去送报纸, 家庭是订阅者, 订了的家庭就能收到新报纸, 一个发布者可以对应多个订阅者

应用:

(1) main.js 导入事件中线插件, 需要先安装插件

import VueBus from 'vue-bus';
Vue.use(VueBus);

(2) demo1 子传父(孙传爷也一样)

  • 父组件订阅事件
  • 子组件发布事件
<template>
  <div>
   <h3>父组件 {{ username }}</h3>
   <hr>
   <Son/>
  </div>
</template>

<script>
import Son from "./Son.vue";
export default {
  components: {
    Son,
  },

  data() {
    return {
      username: "张三",
    };
  },

  created() {
    this.$bus.on("aaa", (data) => {
      this.username = data;
    });
  },
};
</script>
<template>
  <div>
     <button @click="sendEvent">发布事件</button>
  </div>
</template>

<script>
import Son from './Son.vue'
export default {
  components: {
    Son,
  }, 

  methods: {
    sendEvent() { 
      // 发布事件,名称为listEvent
      this.$bus.emit("aaa", '李四');
    },
  },
};
</script>

(2) demo2 跨组件通信(非直属亲属关系)

  • demo.vue订阅aaa事件
  • demo2.vue发布aaa事件

其实不是太适合这种情况, 因为订阅一方得先运行, 而且还不能被销毁(要使用keep-alive)

<!-- demo.vue -->
<template>
  <div>
    <h3>demo1</h3>
    <button @click="sendEvent">发布事件</button>
  </div>
</template>

<script> 
export default {  
  methods: {
    sendEvent() {
      alert('事件发布成功');
      this.$bus.emit("aaa", "李四");
    },
  },
};
</script>
<!-- demo2.vue -->
<template>
  <div>
    <h3>demo2 {{ username }}</h3>
  </div>
</template>

<script>
export default {
  data() {
    return {
      username: "张三",
    };
  },

  created() {
    this.$bus.on("aaa", (data) => {
      this.username = data;
    });
  },
};
</script>

(十一) vue项目性能优化